//
//  GLCoreProfileView.swift
//  TessellatedTriangle
//
//  Created by chenjie on 2019/4/26.
//  Copyright © 2019 starrythrone. All rights reserved.
//

import Cocoa
import OpenGL.GL3

class GLCoreProfileView: NSOpenGLView {
    //MARK: - Propeties
    fileprivate var lifeDuration: CGFloat = 0
    fileprivate var program: GLuint = 0
    fileprivate var vertexArray: GLuint = 0
    
    //MARK: - Life Cycle
    required init?(coder decoder: NSCoder) {
        super.init(coder: decoder)
        
        let openGLContextConfigured = setupOpenGLContext()
        if !openGLContextConfigured {
            print("Prepare OpenGLContextFailed...")
        }
    }

    override init?(frame frameRect: NSRect, pixelFormat format: NSOpenGLPixelFormat?) {
        super.init(frame: frameRect, pixelFormat: format)
        let openGLContextConfigured = setupOpenGLContext()
        if !openGLContextConfigured {
            print("Prepare OpenGLContextFailed...")
        }
    }

    deinit {
        glDeleteProgram(program)
        glDeleteVertexArrays(1, &vertexArray)
    }
    
    //MARK: - Override
    override func prepareOpenGL() {
        super.prepareOpenGL()
        
        if let versionString = glGetString(GLenum(GL_VERSION)) {
            print("GL_VERSION: " + String(cString: versionString))
        }
        if let renderString = glGetString(GLenum(GL_RENDERER)) {
            print("GL_RENDERER: " + String(cString: renderString))
        }
        if let vendorString = glGetString(GLenum(GL_VENDOR)) {
            print("GL_VENDOR: " + String(cString: vendorString))
        }
        if let glVersionString = glGetString(GLenum(GL_SHADING_LANGUAGE_VERSION)) {
            print("GL_SHADING_LANGUAGE_VERSION: " + String(cString: glVersionString))
        }
        
        let shaderLoaded = loadShaders()
        if !shaderLoaded {
            return
        }
        
        glGenVertexArrays(1, &vertexArray);
        glBindVertexArray(vertexArray);
    }
    
    override func reshape() {
        super.reshape()
        let bounds = self.bounds
        glViewport(0, 0, GLsizei(NSWidth(bounds)), GLsizei(NSHeight(bounds)));
    }
    
    
    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        
        let color: [GLfloat] = [0.3, 0.3, 0.3, 1.0]
        glClearBufferfv(GLenum(GL_COLOR), 0, color)
        glUseProgram(program)
        glPolygonMode(GLenum(GL_FRONT_AND_BACK), GLenum(GL_LINE))
        glDrawArrays(GLenum(GL_PATCHES), 0, 3)
        glFlush()
    }
    
    //MARK: - Private Methods
    fileprivate func setupOpenGLContext() -> Bool {
        var pixelFormatAttributes: [NSOpenGLPixelFormatAttribute] =
            [UInt32(NSOpenGLPFAColorSize), 32,
             UInt32(NSOpenGLPFAAccelerated),
             UInt32(NSOpenGLPFAOpenGLProfile), UInt32(NSOpenGLProfileVersion4_1Core), 0]
        guard let pixelFormat = NSOpenGLPixelFormat(attributes: &pixelFormatAttributes) else {
            return false
        }
        
        let currentOpenGLContext = NSOpenGLContext(format: pixelFormat, share: nil)
        self.openGLContext = currentOpenGLContext
        self.openGLContext?.makeCurrentContext()
        return true
    }
    
    fileprivate func loadShaders() -> Bool {
        self.program = glCreateProgram()
        
        var vertexShader: GLuint = 0
//        guard let vertexShaderPath = Bundle.main.path(forResource: "ShaderV", ofType: "vsh") else {
//            print("Can not load vertexShader file")
//            return false
//        }
//        let vertexShaderCompiled = compileShader(shader: &vertexShader, type: GLenum(GL_VERTEX_SHADER), filePath: vertexShaderPath)
//        if (!vertexShaderCompiled) {
//            return false
//        }
        let vertexShaderSource = """
        #version 410 core

        void main() {
            const vec4 vertices[3] = vec4[3](vec4( 0.9, -0.9, 0.5, 1.0),
                                             vec4(-0.9, -0.9, 0.5, 1.0),
                                             vec4( 0.9,  0.9, 0.5, 1.0));
            gl_Position = vertices[gl_VertexID];
        }
        """
        let vertexShaderCompiled = compileShader(shader: &vertexShader, type: GLenum(GL_VERTEX_SHADER), sou: vertexShaderSource)
        if (!vertexShaderCompiled) {
            return false
        }

        var tControlShader: GLuint = 0
//        guard let tControlShaderPath = Bundle.main.path(forResource: "ShaderTC", ofType: "vsh") else {
//            print("Can not load tControlShader file")
//            return false
//        }
//        let tControlShaderCompiled = compileShader(shader: &tControlShader, type: GLenum(GL_TESS_CONTROL_SHADER), filePath: tControlShaderPath)
//        if (!tControlShaderCompiled) {
//            return false
//        }
        let tControlShaderSource = """
        #version 410 core
        layout (vertices = 3) out;
        void main() {
            if (gl_InvocationID == 0) {
                gl_TessLevelInner[0] = 5.0;
                gl_TessLevelOuter[0] = 5.0;
                gl_TessLevelOuter[1] = 5.0;
                gl_TessLevelOuter[2] = 5.0;
            }
            gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
        }
        """
        let tControlShaderCompiled = compileShader(shader: &tControlShader, type: GLenum(GL_TESS_CONTROL_SHADER), sou: tControlShaderSource)
        if (!tControlShaderCompiled) {
            return false
        }

        var tEvaluationShader: GLuint = 0
//        guard let tEvaluationShaderPath = Bundle.main.path(forResource: "ShaderTE", ofType: "vsh") else {
//            print("Can not load tEvaluationShader file")
//            return false
//        }
//        let tEvaluationShaderCompiled = compileShader(shader: &tEvaluationShader, type: GLenum(GL_TESS_EVALUATION_SHADER), filePath: tEvaluationShaderPath)
//        if (!tEvaluationShaderCompiled) {
//            return false
//        }
        let tEvaluationShaderSource = """
        #version 410 core

        layout (triangles, equal_spacing, cw) in;

        void main() {
            vec4 firstVertex = gl_in[0].gl_Position;
            vec4 secondVertex = gl_in[1].gl_Position;
            vec4 thirdVertex = gl_in[2].gl_Position;
            gl_Position = (gl_TessCoord.x * firstVertex + gl_TessCoord.y * secondVertex + gl_TessCoord.z * thirdVertex);
        }
        """
        let tEvaluationShaderCompiled = compileShader(shader: &tEvaluationShader, type: GLenum(GL_TESS_EVALUATION_SHADER), sou: tEvaluationShaderSource)
        if (!tEvaluationShaderCompiled) {
            return false
        }

        var fragmentShader: GLuint = 0
//        guard let fragmentShaderPath = Bundle.main.path(forResource: "ShaderF", ofType: "vsh") else {
//            print("Can not load fragmentShader file")
//            return false
//        }
//        let fragmentShaderCompiled = compileShader(shader: &fragmentShader, type: GLenum(GL_FRAGMENT_SHADER), filePath: fragmentShaderPath)
//        if (!fragmentShaderCompiled) {
//            return false
//        }
        let fragmentShaderSource = """
        #version 410 core
        out vec4 color;
        void main() {
            color = vec4(1.0, 0.0, 1.0, 1.0);
        }
        """
        let fragmentShaderCompiled = compileShader(shader: &fragmentShader, type: GLenum(GL_FRAGMENT_SHADER), sou: fragmentShaderSource)
        if (!fragmentShaderCompiled) {
            return false
        }

        glAttachShader(self.program, vertexShader)
        glAttachShader(self.program, tControlShader)
        glAttachShader(self.program, tEvaluationShader)
        glAttachShader(self.program, fragmentShader)
        
        if (vertexShader != 0) {
            glDeleteShader(vertexShader);
            vertexShader = 0;
        }
        if (tControlShader != 0) {
            glDeleteShader(tControlShader);
            tControlShader = 0;
        }
        if (tEvaluationShader != 0) {
            glDeleteShader(tEvaluationShader);
            tEvaluationShader = 0;
        }
        if (fragmentShader != 0) {
            glDeleteShader(fragmentShader);
            fragmentShader = 0;
        }

        let programLinked = linkProgram(program)
        if programLinked {
            return true
        } else {
            return false
        }
    }

    fileprivate func compileShader(shader: inout GLuint, type: GLenum, sou: String) -> Bool {
        var source: UnsafePointer<Int8>? = (sou as NSString).utf8String
        var shaderSource = UnsafePointer<GLchar>(source)
        if (shaderSource == nil) {
            return false
        }

        shader = glCreateShader(type)
        glShaderSource(shader, 1, &shaderSource, nil)
        glCompileShader(shader)

        var status: GLint = 0
        glGetShaderiv(shader, GLenum(GL_COMPILE_STATUS), &status)
        if status == 0 {
            var logLength: GLint = 0
            glGetShaderiv(shader, GLenum(GL_INFO_LOG_LENGTH), &logLength)
            var infoLog = [GLchar](repeating: 0, count: Int(logLength))
            glGetShaderInfoLog(shader, logLength, nil, &infoLog)
            let infoLogString = String(NSString.init(utf8String: &infoLog) ?? "")
            glDeleteShader(shader)
            return false
        }
        return true
    }

    fileprivate func compileShader(shader: inout GLuint, type: GLenum, filePath: String) -> Bool {
        var source: UnsafePointer<Int8>?
        do {
            source = try NSString(contentsOfFile: filePath, encoding: String.Encoding.utf8.rawValue).utf8String
        } catch {
            print("Failed to load shader string...")
            return false
        }
        var shaderSource = UnsafePointer<GLchar>(source)
        if (shaderSource == nil) {
            return false
        }
        
        shader = glCreateShader(type)
        glShaderSource(shader, 1, &shaderSource, nil)
        glCompileShader(shader)

        var status: GLint = 0
        glGetShaderiv(shader, GLenum(GL_COMPILE_STATUS), &status)
        if status == 0 {
            var logLength: GLint = 0
            glGetShaderiv(shader, GLenum(GL_INFO_LOG_LENGTH), &logLength)
            var infoLog = [GLchar](repeating: 0, count: Int(logLength))
            glGetShaderInfoLog(shader, logLength, nil, &infoLog)
            let infoLogString = String(NSString.init(utf8String: &infoLog) ?? "")
            print("Compile shader failed at file:\n" + filePath + "\nwith log\n" + infoLogString)
            glDeleteShader(shader)
            return false
        }
        return true
    }
    
    fileprivate func linkProgram(_ program: GLuint) -> Bool {
        glLinkProgram(program)
        var status: GLint = 0
        glGetProgramiv(program, GLenum(GL_LINK_STATUS), &status)
        if status == 0 {
            var infoLength: GLint = 0
            glGetProgramiv(program, GLenum(GL_INFO_LOG_LENGTH), &infoLength)
            var infoLog = [GLchar](repeating: 0, count: Int(infoLength))
            glGetProgramInfoLog(program, infoLength, nil, &infoLog)
            let infoLogString = String(NSString.init(utf8String: &infoLog) ?? "")
            print("Like program fiaied with log: " + infoLogString)
            return false
        }
        return true
    }
}
